iT邦幫忙

2026 iThome 鐵人賽

DAY 6
0
佛心分享-IT 人技術創業

Berry AI:從零開始打造全美第一的得來速 Vision AI系列 第 6

如何連上橫跨美洲大陸的千百台 Edge Servers?什麼是反向代理?Rathole 部署實戰!

  • 分享至 

  • xImage
  •  

Berry AI 的一些產品功能需要即時 (realtime) 在門市內計算完成,甚至在門市的對外網路故障時也要繼續運作,因此我們在每一間店都有設置一台 edge server,接上客戶管理的內部網路,直接在當地提供 AI 功能。

這樣的架構有不少技術難題,其中之一就是 — 怎麼連上位於客戶內網的 edge server?更進一步說,怎麼在千百個環境都略有不同的門市中,都能與 edge server 建立安全且穩定的連線,讓工程師可以遠端更新、維護與除錯呢?

畢竟我們部署的門市實在太多,美國也實在太大啦!有些門市甚至位於開車要數個小時才能抵達的沙漠小鎮當中,總不可能每次有什麼問題都要派人跑到現場解決吧…

藏身在路由器之後的 Edge Server

承前段所說,我們的 edge server 安裝在客戶的內網當中,並與外部的網路相隔著一個路由器 (router)。

路由器其中一個關鍵功能就是網路位址轉譯 (Network Address Translation, NAT) — 將內網 IP + 內網 port 的組合,轉譯為外網 IP (就是路由器的對外 IP) + 外網 port 的組合。所有的轉譯規則都被記錄在路由器內的轉譯表 (translation table) 當中,可是只有主動由內對外 (outbound) 的網路流量會產生轉譯規則,也就是說,要由內網的裝置主動對外發起連線,表上才會記下一筆對應;等外網服務回應封包時,路由器才知道要轉送回內網的哪一台裝置。

但是,當外網的裝置想主動反過來建立連線時,因為轉譯表上沒有對應的轉譯規則,路由器就會放棄轉送並丟棄封包。這裡沒有防火牆在作用,光是路由器加上 NAT 就讓內網裝置收不到外部未經邀請的連入請求,不過這也給工程師帶來了莫大的困擾 — 我們如何才能連上 edge server 呢?畢竟程式還是要更新、服務還是要維護呀!

NAT 轉譯表由對外流量建立,因此連線只能由內網側發起;外部主動連入時查無對應而被丟棄

NAT 的單向性:往外走得通,往內走不通。

反向代理:讓 edge server 主動建立連線

既然轉譯規則只有在連線由內網發起時會產生,那就讓 edge server 自己主動連出去,連上一台我們控制的伺服器就好啦!這台伺服器需要有固定的外網 IP,它要做的事情也很單純,就是待在那裡等各家門市來報到。門市內的 edge server 一開機就主動連上它,接通之後就一直保持著不斷線。因為連線是由門市內網發起的,路由器的轉譯表上就會有記錄,封包的來回傳送也就都沒問題囉!

接下來是重點。工程師要連上某一間門市的時候,連的其實是這台中繼伺服器。這台伺服器會為每一台 edge server 的 sshd 各開一個專屬的 port,工程師只要 ssh 到對應的 port,流量就會沿著那早就掛好的連線,送到運行在門市內的 sshd 上面。

而這台中繼伺服器在做的事情,就是反向代理 (Reverse Proxy):它代替真正提供服務 (此例中是 sshd) 的那台 edge server 接受連線,收到之後再轉發給它。

但是跟常見的反向代理架構有個小差別。通常作為下游 (downstream) 的反向代理與上游 (upstream) 的服務是放在同一個內部子網路當中,由代理主動向上游發起連線;而我們的上游服務躲在客戶路由器後面,因為 NAT 的緣故無法被主動連上,所以只好改由 edge server 自己主動先連線,之後代理要轉發的流量就走這條現成的連線,它有個通俗的名稱 — Tunnel

「反向」代理的角色其實沒有改變,變的只是那條通往上游的連線,是由哪一邊先發起的而已。

門市的客戶端主動連出並維持連線,工程師則透過中繼伺服器上對應的 port 進入門市內網

箭頭方向是這張圖的重點:連線一律由門市側發起。圖中的 Rathole 與 Caddy 是後面才會介紹的實作,這裡可以先略過。

開源的反向代理該選用哪一個?

反向代理的選擇其實不少,光是 GitHub 上星星破萬的就有好幾個,那應該挑哪一個呢?我們的需求有下面這三項:

  1. 客戶端獨立 token:每個客戶端 (client) 都要使用專屬的 token 進行認證。門市會開業也會歇業,設備也有送修更換的時候,所以我們必須能夠單獨移除個別設備的存取權限,而不是給所有門市的設備都重新設定新 token。而且萬一有 token 不小心外流,這樣的設計也能把影響範圍限縮在個別設備上面。
  2. port 由代理端決定:每個上游服務對應的 port 都由代理端指定。如果讓上游服務自己選擇想要的 port 號碼,管理上會非常困難,畢竟客戶端之間無法相互溝通,搞不清楚哪些 port 已經被使用而哪些又是空置的,一律改由代理端集中設定才不會亂。
  3. 撐得住上千條連線:要能承載上千台 edge server 同時連線。每一條連線都要佔掉一些記憶體與計算資源,幾十個連線可能還沒什麼感覺,但要穩定撐住上千個連線就不是那麼簡單了。所以效能要好、記憶體要省,而且資源用量的成長要是線性可預期的。

依照這三項需求,檢視幾個比較熱門的反向代理:

工具 客戶端獨立 token port 由誰決定 實作
rathole 代理端 Rust
frp 否,全域共用 客戶端 Go
nps 代理端 Go

先說 frp,它的生態是這幾個裡面最完整的,GitHub 星星數也遙遙領先。但是它預設的驗證 token 是所有客戶端共用同一個,除非使用 OIDC 客戶端才能得到專屬的 token,單純為了這件事而多引進一套驗證系統實在不太划算。更麻煩的是 frp 要開哪一個 port 是寫在客戶端的設定檔裡的,代理端只能圈出一個允許的範圍,條件 1 與 2 都不滿足。

至於 nps 在前兩項其實都符合,可惜它最後一次更新停在 2024 年 5 月,兩年多都沒有動靜了,issue 也累積了五百多個沒有處理,這種狀態實在不太適合放進正式環境…

最後是用 Rust 寫的 rathole,官方的 基準測試 顯示它在高併發下比 frp 穩定得多,資源使用量也更少 (但這個測試的年代有點久遠了,參考就好)。rathole 也支援個別客戶端使用獨立 token (其實還能更細,每個服務都可以有自己的 token,後面會再說明) 以及由代理端決定 port 的功能,就決定是你啦!

動手玩:五分鐘架起一套 rathole

講了這麼多,不如直接跑一次。我們使用容器 (container) 與 Docker Compose 把前面那張架構圖實作出來!

網路環境

整個 lab 有三個網段,剛好對應真實部署的三個位置:

網段 網段中的容器
internet (外網) gatewayedge01-rathole-client
hq (公司機房) gatewayrathole-server
store (門市內網) edge01-rathole-clientedge01-sshd

表格裡的 gateway 是由 Caddy 負責,它站在 rathole-server 前面,是公司機房對外的唯一入口。它會做兩件事:處理 TLS 加解密,以及把來自 edge01-rathole-client 的流量轉送給內部的 rathole-server。正式環境裡憑證是 Caddy 用 ACME 申請的,rathole 完全不碰 TLS。但是這個 lab 為了簡化並沒有真的啟用 HTTPS,而是將明文的 HTTP 跑在 port 443 上。

edge01-sshd 是對應實際環境裡 sshd 的位置,不過這個 lab 使用 Caddy 在 :22 代打,簡單模擬一下情況。另外,它只掛在 store 網段上,也沒有發布任何 port 到主機 (host) 上。

這裡最值得注意的是,rathole-serveredge01-rathole-client 完全沒有共用任何網段。Tunnel 之所以能通,純粹是靠客戶端自己主動連上 gateway 的呀!

設定代理端 (rathole-server)

server.toml 要定義的是客戶端連進來的位址,以及打算把哪些服務暴露出來。至於 transport 為什麼選 websocket,留到文末的補充再談:

[server]
bind_addr = "0.0.0.0:7000"

[server.transport]
type = "websocket"

[server.transport.websocket]
# 這層不做 TLS,正式環境中前面的 Caddy 已經是 HTTPS 終點
tls = false

[server.services.edge01_ssh]
# 每個 service 一組獨立 token,這是選 rathole 的第一個理由
token = "d06-lab-demo-token-please-change"
# 工程師要連的就是這個 port,寫在 server 這一側,這是第二個理由
bind_addr = "0.0.0.0:10022"

另外,前文並沒有仔細說明設備、客戶端、服務的對應關係,在這邊可以看到 rathole 的設定單位是服務 (service) 而不是設備或客戶端,tokenbind_addr 都掛在 [server.services.*] 底下。也就是說一台 edge server 上如果要同時替 SSH 和 Web API 打洞,那就是兩個服務、兩組 token、兩個 port。不過我們每一間門市目前都只開 SSH tunnel,一切都剛好一對一,所以前面為了方便理解,就沒有特別區分說明了。

設定客戶端 (edge01-rathole-client)

client.toml 是對稱的一份設定,差別在於它要指定把哪個本機服務送出去:

[client]
# 連的是 gateway,而不是直接連 rathole server
remote_addr = "tunnel.example.com:443"

[client.transport]
type = "websocket"

[client.transport.websocket]
# lab 走純 HTTP,所以是 false
# 正式環境改成 true,並在 [client.transport.tls] 指定 trusted_root
tls = false

[client.services.edge01_ssh]
token = "d06-lab-demo-token-please-change"
# 要打洞出去的 sshd。正式環境是 127.0.0.1:22
local_addr = "edge01-sshd:22"

local_addr 是整份設定裡最值得留意的一行。它是從客戶端的角度看出去的位址,正式環境換成 127.0.0.1:22,就是文章開頭那個情境了。另外,服務名稱 edge01_ssh 必須與代理端的設定一致,不然會註冊不上去唷!

啟動與驗證

compose.yaml 負責把四個容器接起來:

name: d06-rathole-lab

networks:
  internet: # 外網
  hq: # 公司機房
  store: # 門市內網

services:
  gateway:
    image: caddy:2-alpine
    networks:
      internet:
        # client 就是依照這個域名連線的
        aliases: ["tunnel.example.com"]
      hq:
    # 為了簡化 lab 的設定,這裡其實並沒有使用 HTTPS
    command: ["caddy", "reverse-proxy", "--from", ":443", "--to", "rathole-server:7000"]

  rathole-server:
    image: rapiz1/rathole:v0.5.0
    platform: linux/amd64
    networks: [hq]
    # 實際環境中,代理伺服器與工程師的筆電位於同一個網段,可以直接連線
    # 這個 lab 裡改用 port forwarding 達到同樣的效果
    ports: ["10022:10022"]
    volumes: ["./server.toml:/config.toml:ro"]
    command: ["--server", "/config.toml"]

  # 正式環境中,rathole-client 與 sshd 都跑在同一台 edge server 上
  edge01-rathole-client:
    image: rapiz1/rathole:v0.5.0
    platform: linux/amd64
    # 有掛 internet 所以能從門市內網連到外網
    networks: [internet, store]
    volumes: ["./client.toml:/config.toml:ro"]
    command: ["--client", "/config.toml"]
    depends_on: [gateway]

  edge01-sshd:
    image: caddy:2-alpine
    # 沒有 internet,只能透過反向代理與 tunnel 來連上
    networks: [store]
    command: ["caddy", "respond", "--listen", ":22", "--body", "ssh: connected to edge01"]

啟動之後先看客戶端的 log,確認它有順利連上 rathole-server

docker compose up -d && sleep 5
docker compose logs edge01-rathole-client --tail 3
edge01-rathole-client-1  | 2026-09-02T03:46:58.890385Z  INFO handle{service=edge01_ssh}: rathole::client: Starting f3c68b2065b00ebcd66720b7880a73f54fd481799cef63a9afdda50462a95621
edge01-rathole-client-1  | 2026-09-02T03:46:58.891952Z  INFO config_watcher{path="/config.toml"}: rathole::config_watcher: Start watching the config
edge01-rathole-client-1  | 2026-09-02T03:46:58.913442Z  INFO handle{service=edge01_ssh}:run: rathole::client: Control channel established

最後那行 Control channel established,代表門市內的 edge server 已經主動連出去,而且接通了。接著再跑一個檢查:

curl -s localhost:10022

會得到:

ssh: connected to edge01

順利連上藏身在門市內網的 edge server 了!

補充:為什麼 transport 選 WebSocket,而不是 TCP、TLS 或者 Noise?

rathole 的 transport 可以選 TCP、TLS、Noise 或者 WebSocket,我們選 WebSocket 有兩個原因:

  • WebSocket + TLS 一般是使用 port 443,交握 (handshake) 用的是一個標準的 HTTP Upgrade 請求,所以在防火牆與深層封包檢測 (Deep Packet Inspection, DPI) 眼中,它跟一般的 HTTPS 流量長得沒什麼兩樣,比較不容易被攔截阻擋。
  • WebSocket 是 L7 協定,可以直接掛在既有的 Caddy 或者 Traefik 後面,跟公司其他服務共用入口,憑證則交給 ACME 自動處理。如果是用 TCP、TLS 或者 Noise transport,那就得為它獨佔一個 port,也沒辦法跟 L7 反向代理共同管理了。

使用 WebSocket 的代價是傳輸上會需要多帶一些協定 header,但日常用來 SSH 其實感覺不出差別。另外要記得的是 heartbeat 不能關掉,因為 L7 代理通常會回收閒置太久的連線,所以 heartbeat 的間隔必須要短於回收的 timeout 唷!

小結

回頭看,整套架構的關鍵就是:既然 NAT 決定了連線只能由內往外發起,那就讓 edge server 自己從內網連上反向代理 — 建立 tunnel,工程師就能由 tunnel 轉進門市之中啦!想通這件事之後,剩下的真的都只是設定檔的問題了。

不過這個架構也有一些問題:反向代理是個單點 (Single Point of Failure, SPOF),它一故障,所有門市的設備就會全部一起失聯;另外,門市數量再成長到上萬間的時候,單台伺服器恐怕也難以承受所有流量,必須要做水平擴容 (horizontal scaling) 才行。這兩項都是還沒解決的工程挑戰,就等我們未來解決再分享囉。

參考資料


本系列由 Berry AI 工程團隊出品。更多工程實戰紀錄都在 Berry AI 技術部落格


上一篇
上千台 Edge Server 的 OS 全自動安裝:autoinstall 與 LUKS + TPM 全碟加密一次到位!
下一篇
影像串流一把罩,最強開源 Media Server — MediaMTX
系列文
Berry AI:從零開始打造全美第一的得來速 Vision AI10
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言